9.4 Список Задач + Dependency Inversion Principle
4 из 4 шагов пройдено

 Список Задач + Dependency Inversion Principle

➡️Ссылка на репозиторий с кодом этого урока

Модель и UI без изменений

class Task {  
  int id;  
  String text;  
  bool isDone;  
  
  Task({required this.id, required this.text, this.isDone = false});  
  
  // Фабричный конструктор для создания Task из Map (JSON)  
  factory Task.fromJson(Map<String, dynamic> json) {  
    return Task(  
      id: json['id'],  
      text: json['text'],  
      isDone: json['isDone'],  
    );  
  }  
  
  // Метод для преобразования Task в Map (JSON)  
  Map<String, dynamic> toJson() {  
    return {  
      'id': id,  
      'text': text,  
      'isDone': isDone,  
    };  
  }  
  
  // Метод для копирования объекта с определенными значениями  
  Task copyWith({  
    int? id,  
    String? text,  
    bool? isDone,  
  }) {  
    return Task(  
      id: id ?? this.id,  
      text: text ?? this.text,  
      isDone: isDone ?? this.isDone,  
    );  
  }  
}
import 'package:flutter/material.dart';  
import 'package:provider/provider.dart';  
import '../models/task.dart';  
import '../viewmodels/todo_viewmodel.dart';  
  
class ToDoScreen extends StatelessWidget {  
  const ToDoScreen({super.key});  
  
  @override  
  Widget build(BuildContext context) {  
    debugPrint("🔴 ToDoScreen build");  
    // Следим только за количеством задач  
    // Чтобы каждый раз не перестраивать ListView    final taskCount = context.select<ToDoViewModel, int>(  
          (vm) => vm.tasks.length,  
    );  
  
    return Scaffold(  
      appBar: AppBar(  
        title: const Text('Список Задач'),  
        actions: [  
          // Используем Consumer, чтобы лишний раз не перестраивать ListView  
          Consumer<ToDoViewModel>(  
            builder: (context, vm, child) => Row(  
              children: [  
                Center(  
                  child: Text(vm.isDarkMode ? 'Темная тема' : 'Светлая тема'),  
                ),  
                Switch(  
                  value: vm.isDarkMode,  
                  // Используем context.read для вызова метода без подписки  
                  onChanged: (_) => context.read<ToDoViewModel>().toggleTheme(),  
                ),  
                const SizedBox(width: 8),  
              ],  
            ),  
          ),  
        ],  
      ),  
      body: ListView.builder(  
        itemCount: taskCount, // Количество задач  
        itemBuilder: (context, index) {  
          final tasks = context.read<ToDoViewModel>().tasks;  
          final task = tasks[index];  
  
          return TaskItem(key: ValueKey(task.id), taskId: task.id);  
        },  
      ),  
  
      floatingActionButton: FloatingActionButton.extended(  
        onPressed: () => _showAddTaskDialog(context),  
        label: const Text('Добавить'),  
        icon: const Icon(Icons.add),  
      ),  
    );  
  }  
  
  // Показать диалоговое окно  
  void _showAddTaskDialog(BuildContext context) {  
    final vm = context.read<ToDoViewModel>();  
  
    showDialog(  
      context: context,  
      builder: (context) {  
        return AlertDialog(  
          title: const Text('Добавить задачу'),  
          content: TextField(  
            controller: vm.textEditingController,  
            autofocus: true,  
            decoration: const InputDecoration(hintText: "Введите текст задачи"),  
          ),  
          actions: [  
            TextButton(  
              onPressed: () => Navigator.of(context).pop(),  
              child: const Text('Отмена'),  
            ),  
            TextButton(  
              onPressed: () {  
                vm.addTask(vm.textEditingController.text);  
                vm.textEditingController.clear();  
                Navigator.of(context).pop();  
              },  
              child: const Text('Добавить'),  
            ),  
          ],  
        );  
      },  
    );  
  }  
}  
  
/// Карточка задачи  
class TaskItem extends StatelessWidget {  
  final int taskId;  
  const TaskItem({super.key, required this.taskId});  
  
  @override  
  Widget build(BuildContext context) {  
    debugPrint("🔴 TaskItem build");  
    // Используем Selector, чтобы подписаться на изменения ТОЛЬКО одной задачи.  
    // selector извлекает конкретную задачу по ID.
	// builder будет вызван только если объект этой задачи изменился
	return Selector<ToDoViewModel, Task>(  
      // 1. Выбирает конкретный объект Task из списка по заданному ID  
      selector: (_, viewModel) =>  
          viewModel.tasks.firstWhere((task) => task.id == taskId),  
      // 2.Взывается только тогда, когда выбранный Task изменился  
      builder: (context, task, child) {  
        // 3. Используется для вызова методов, не подписываясь на обновления.  
        final vm = context.read<ToDoViewModel>();  
  
        return Container(  
          margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),  
          decoration: BoxDecoration(  
            border: Border.all(color: Colors.grey),  
            borderRadius: BorderRadius.circular(8),  
          ),  
          child: ListTile(  
            leading: Checkbox(  
              value: task.isDone,  
              onChanged: (value) {  
                vm.updateTaskStatus(task.id, value ?? false);  
              },  
            ),  
            title: Row(  
              children: [  
                Text("${task.id}"),  
                SizedBox(width: 8),  
                Text(  
                  task.text,  
                  style: TextStyle(  
                    decoration: task.isDone  
                        ? TextDecoration.lineThrough  
                        : TextDecoration.none,  
                  ),  
                ),  
              ],  
            ),  
            trailing: IconButton(  
              icon: const Icon(Icons.delete_outline, color: Colors.cyan),  
              onPressed: () {  
                vm.deleteTask(task.id);  
              },  
            ),  
          ),  
        );  
      },  
    );  
  }  
}

Будьте вежливы и соблюдайте наши принципы сообщества. Пожалуйста, не оставляйте решения и подсказки в комментариях, для этого есть отдельный форум.
Оставить комментарий